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:
> I'd also really like to know @thomasjpfan's opinion on that, because I have the impression that the original intention of available_if was rather to have the meta-estimator display AttribributeError if their sub-estimators don't implement a method.

The design of `available_if` was to be flexible and allow the sub-estimator to raise or the meta-estimator.

> Though, OneVsRestClassifier does implement partial_fit, but the underlying estimator doesn't.

Although `OneVsRestClassifier.partial_fit` is implemented in the code, it is dynamically removed from the instance if the underlying estimator does not define `partial_fit`. In that sense, the `AttributeError` about `OneVsRestClassifier` is correct, but it is not informative enough to explain "why it is not defined".

As an overall solution, I would go with https://github.com/scikit-learn/scikit-learn/issues/28108#issuecomment-1889780471, but also improve `available_if` to reraise the AttributeError. That way, we get the both `AttributeErrors` from the meta-estimator and the sub-estimator. For example:

```python
from sklearn.pipeline import make_pipeline
from sklearn.tree import DecisionTreeClassifier
from sklearn.datasets import load_iris

X, y = load_iris(return_X_y=True)
clf = make_pipeline(DecisionTreeClassifier()).fit(X, y)
clf.decision_function(X)
```

raises the following:

<details><summary>Exception</summary>

```python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
File ~/Repos/scikit-learn-1/sklearn/utils/_available_if.py:29, in _AvailableIfDescriptor._check(self, obj, owner)
     28 try:
---> 29     check_result = self.check(obj)
     30 except Exception as e:

File ~/Repos/scikit-learn-1/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'

The above exception was the direct cause of the following exception:

AttributeError                            Traceback (most recent call last)
Cell In[2], line 7
      5 X, y = load_iris(return_X_y=True)
      6 clf = make_pipeline(DecisionTreeClassifier()).fit(X, y)
----> 7 clf.decision_function(X)

File ~/Repos/scikit-learn-1/sklearn/utils/_available_if.py:40, in _AvailableIfDescriptor.__get__(self, obj, owner)
     36 def __get__(self, obj, owner=None):
     37     if obj is not None:
     38         # delegate only on instances, not the classes.
     39         # this is to allow access to the docstrings.
---> 40         self._check(obj, owner=owner)
     41         out = MethodType(self.fn, obj)
     43     else:
     44         # This makes it possible to use the decorated method as an unbound method,
     45         # for instance when monkeypatching.

File ~/Repos/scikit-learn-1/sklearn/utils/_available_if.py:31, in _AvailableIfDescriptor._check(self, obj, owner)
     29     check_result = self.check(obj)
     30 except Exception as e:
---> 31     raise AttributeError(attr_err_msg) from e
     33 if not check_result:
     34     raise AttributeError(attr_err_msg)

AttributeError: This 'Pipeline' has no attribute 'decision_function'
```

</details>

I opened https://github.com/scikit-learn/scikit-learn/pull/28198 with this proposal.

Labels: Bug