BUG Wrong error raised for OneVsRestClassifier with a sub-estimator that doesn't allow partial_fit

### Describe the bug

When using the `OneVsRestClassifier` with a sub-estimator, that doesn't have `partial_fit` implemented, a misleading error message is shown: `AttributeError: This 'OneVsRestClassifier' has no attribute 'partial_fit'`.

Though, `OneVsRestClassifier` does implement `partial_fit`, but the underlying estimator doesn't.

There is an appropriate error raising already implemented in
https://github.com/scikit-learn/scikit-learn/blob/f1e89363f6777155a25b3574db9f0fc5c21a8c51/sklearn/multiclass.py#L437

```python
            if not hasattr(self.estimator, "partial_fit"):
                raise ValueError(
                    ("Base estimator {0}, doesn't have partial_fit method").format(
                        self.estimator
                    )
                )
```
But it's not run, because the AttributeError from the `@available_if` decorator/descriptor thing pops up earlier.

I'd like to learn about that, repair that and add a test to make sure this doesn't happen again by accident.
I will also check if other methods from the multiclass classifiers are also affected.

Is it alright if I go ahead with this?

### Steps/Code to Reproduce

```python
from sklearn.datasets import load_iris
from sklearn.multiclass import OneVsRestClassifier
from sklearn.linear_model import LogisticRegression
import numpy as np

iris = load_iris()
sample_weight = np.ones_like(iris.target, dtype=np.float64)
clf = OneVsRestClassifier(
estimator=LogisticRegression(random_state=42)
)
clf.partial_fit(iris.data, iris.target)
```

### Expected Results

`ValueError:  LogisticRegression doesn't have partial_fit method`

### Actual Results

`AttributeError: This 'OneVsRestClassifier' has no attribute 'partial_fit'`.

### Versions

```shell
System:
    python: 3.10.6 (main, Oct 10 2022, 12:43:33) [GCC 9.4.0]
executable: /home/stefanie/.pyenv/versions/3.10.6/envs/scikit-learn_dev/bin/python
   machine: Linux-5.15.0-91-generic-x86_64-with-glibc2.31

Python dependencies:
      sklearn: 1.5.dev0
          pip: 23.3.2
   setuptools: 63.2.0
        numpy: 1.26.0
        scipy: 1.11.3
       Cython: 0.29.34
       pandas: 2.1.4
   matplotlib: 3.8.0
       joblib: 1.3.2
threadpoolctl: 3.1.0

Built with OpenMP: True

threadpoolctl info:
       user_api: blas
   internal_api: openblas
         prefix: libopenblas
       filepath: /home/stefanie/.pyenv/versions/3.10.6/envs/scikit-learn_dev/lib/python3.10/site-packages/numpy.libs/libopenblas64_p-r0-0cf96a72.3.23.dev.so
        version: 0.3.23.dev
threading_layer: pthreads
   architecture: SkylakeX
    num_threads: 8

       user_api: blas
   internal_api: openblas
         prefix: libopenblas
       filepath: /home/stefanie/.pyenv/versions/3.10.6/envs/scikit-learn_dev/lib/python3.10/site-packages/scipy.libs/libopenblasp-r0-23e5df77.3.21.dev.so
        version: 0.3.21.dev
threading_layer: pthreads
   architecture: SkylakeX
    num_threads: 8

       user_api: openmp
   internal_api: openmp
         prefix: libgomp
       filepath: /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0
        version: None
    num_threads: 8
```

COMMENT:
Interesting, I never realize this issue. `available_if` is taking a `check` as parameter and this parameter can return `True` or `False` but also raise an `AttributeError`.

When returning `False`, it implies that the original object will raise an `AttributeError` (in our case this is `OneVsRestClassifier`. If we want the underlying estimator to raise the error then we need to do so in the `check` function.

This is already something that we do with the `Pipeline` class:

https://github.com/scikit-learn/scikit-learn/blob/30fcb45f14e761ad41ce1e8874ee7e33b8fa7f79/sklearn/pipeline.py#L46-L56

With this pattern, we get the right error with the following example:

```python
from sklearn.pipeline import make_pipeline
from sklearn.tree import DecisionTreeClassifier

X, y = load_iris(return_X_y=True)
clf = make_pipeline(DecisionTreeClassifier()).fit(X, y)
clf.decision_function(X)
```

```pytb
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[21], line 6
      4 X, y = load_iris(return_X_y=True)
      5 clf = make_pipeline(DecisionTreeClassifier()).fit(X, y)
----> 6 clf.decision_function(X)

File ~/Documents/packages/scikit-learn/sklearn/utils/_available_if.py:31, in _AvailableIfDescriptor.__get__(self, obj, owner)
     25 attr_err = AttributeError(
     26     f"This {repr(owner.__name__)} has no attribute {repr(self.attribute_name)}"
     27 )
     28 if obj is not None:
     29     # delegate only on instances, not the classes.
     30     # this is to allow access to the docstrings.
---> 31     if not self.check(obj):
     32         raise attr_err
     33     out = MethodType(self.fn, obj)

File ~/Documents/packages/scikit-learn/sklearn/pipeline.py:53, in _final_estimator_has.<locals>.check(self)
     51 def check(self):
     52     # raise original `AttributeError` if `attr` does not exist
---> 53     getattr(self._final_estimator, attr)
     54     return True

AttributeError: 'DecisionTreeClassifier' object has no attribute 'decision_function'
``` 

So it means that we should change the following pattern:

https://github.com/scikit-learn/scikit-learn/blob/30fcb45f14e761ad41ce1e8874ee7e33b8fa7f79/sklearn/multiclass.py#L180-L189

by something like:

```python
def _estimators_has(attr):
    """Check if self.estimator or self.estimators_[0] has attr.

    If `self.estimators_[0]` has the attr, then its safe to assume that other
    values has it too. This function is used together with `available_if`.
    """
    def check(self):
        if hasattr(self, "estimators_"):
            getattr(self.estimators_[0], attr)
            return True
        else:
            getattr(self.estimator, attr)
            return True
    return check
```

`getattr` will raise an `AttributeError` for `estimator` or `estimators_[0]` that is the underlying estimator.

I think that we should search for the pattern `return lambda self:` in the repository and check that the estimators implemented it do not suffer from the same bug.

@StefanieSenger do you want to solve this issue?

Labels: Bug